package net.sector.fonts;


import static net.sector.util.Align.*;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sector.Constants;
import net.sector.util.Log;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;

import com.porcupine.color.RGB;
import com.porcupine.coord.Coord;


/**
 * A TrueType font implementation originally for Slick, edited for Bobjob's
 * Engine
 * 
 * @original author James Chambers (Jimmy)
 * @original author Jeremy Adams (elias4444)
 * @original author Kevin Glass (kevglass)
 * @original author Peter Korzuszek (genail)
 * @new version edited by David Aaron Muhar (bobjob)
 * @new version edited by MightyPork
 */
@SuppressWarnings("javadoc")
public class LoadedFont {

	private static final boolean DEBUG = Constants.LOG_FONTS;

	/** Map of user defined font characters (Character <-> IntObject) */
	private Map<Character, CharStorageEntry> chars = new HashMap<Character, CharStorageEntry>(100);

	/** Boolean flag on whether AntiAliasing is enabled or not */
	private boolean antiAlias;

	/** Font's size */
	private int fontSize = 0;

	/** Font's height */
	private int fontHeight = 0;

	/** Texture used to cache the font 0-255 characters */
	private int fontTextureID;

	/** Default font texture width */
	private int textureWidth = 2048;

	/** Default font texture height */
	private int textureHeight = 2048;

	/** A reference to Java's AWT Font that we create our font texture from */
	private Font font;

	/** The font metrics for our Java AWT font */
	private FontMetrics fontMetrics;

	private int correctL = 9, correctR = 8;

	private double defScaleX = 1, defScaleY = 1;
	private double clipVerticalT = 0;
	private double clipVerticalB = 0;

	private class CharStorageEntry {
		/** Character's width */
		public int width;

		/** Character's height */
		public int height;

		/** Character's stored x position */
		public int texPosX;

		/** Character's stored y position */
		public int texPosY;
	}


	public LoadedFont(Font font, boolean antiAlias, String charsNeeded) {
		this.font = font;
		this.fontSize = font.getSize() + 3;
		this.antiAlias = antiAlias;

		createSet(charsNeeded.toCharArray());

		fontHeight -= 1;
		if (fontHeight <= 0) fontHeight = 1;
	}

//	public TextureFont(Font font, boolean antiAlias) {
//		this(font, antiAlias, null);
//	}

	public void setCorrection(boolean on) {
		if (on) {
			correctL = 2;
			correctR = 1;
		} else {
			correctL = 0;
			correctR = 0;
		}
	}

	private BufferedImage getFontImage(char ch) {
		// Create a temporary image to extract the character's size
		BufferedImage tempfontImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = (Graphics2D) tempfontImage.getGraphics();
		if (antiAlias == true) {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		}
		g.setFont(font);
		fontMetrics = g.getFontMetrics();
		int charwidth = fontMetrics.charWidth(ch) + 8;

		if (charwidth <= 0) {
			charwidth = 7;
		}
		int charheight = fontMetrics.getHeight() + 3;
		if (charheight <= 0) {
			charheight = fontSize;
		}

		// Create another image holding the character we are creating
		BufferedImage fontImage;
		fontImage = new BufferedImage(charwidth, charheight, BufferedImage.TYPE_INT_ARGB);
		Graphics2D gt = (Graphics2D) fontImage.getGraphics();
		if (antiAlias == true) {
			gt.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		}
		gt.setFont(font);

		gt.setColor(Color.WHITE);
		int charx = 3;
		int chary = 1;
		gt.drawString(String.valueOf(ch), (charx), (chary) + fontMetrics.getAscent());

		return fontImage;

	}


	private void createSet(char[] charsToLoad) {

		try {
			class LoadedGlyph {
				public char c;
				public BufferedImage image;
				public int width;
				public int height;

				public LoadedGlyph(char c, BufferedImage image) {
					this.image = image;
					this.c = c;
					this.width = image.getWidth();
					this.height = image.getHeight();
				}
			}

			List<LoadedGlyph> glyphs = new ArrayList<LoadedGlyph>();
			List<Character> loaded = new ArrayList<Character>();
			for (char ch : charsToLoad) {
				if (!loaded.contains(ch)) {
					glyphs.add(new LoadedGlyph(ch, getFontImage(ch)));
					loaded.add(ch);
				}
			}

			Coord canvas = new Coord(128, 128);
			double lineHeight = 0;
			Coord begin = new Coord(0, 0);
			boolean needsLarger = false;

			while (true) {

				needsLarger = false;

				for (LoadedGlyph glyph : glyphs) {
					if (begin.x + glyph.width > canvas.x) {
						begin.y += lineHeight;
						lineHeight = 0;
						begin.x = 0;
					}

					if (lineHeight < glyph.height) {
						lineHeight = glyph.height;
					}

					if (begin.y + lineHeight > canvas.y) {
						needsLarger = true;
						break;
					}

					// draw.
					begin.x += glyph.width;
				}

				if (needsLarger) {
					canvas.x *= 2;
					canvas.y *= 2;
					begin.setTo(0, 0);
					lineHeight = 0;
				} else {
					if (DEBUG) Log.f3("Preparing texture " + canvas.x + "x" + canvas.y);
					break;
				}
			}

			textureWidth = (int) canvas.x;
			textureHeight = (int) canvas.y;

			BufferedImage imgTemp = new BufferedImage(textureWidth, textureHeight, BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = (Graphics2D) imgTemp.getGraphics();

			g.setColor(new Color(0, 0, 0, 1));
			g.fillRect(0, 0, textureWidth, textureHeight);

			int rowHeight = 0;
			int positionX = 0;
			int positionY = 0;

			for (LoadedGlyph glyph : glyphs) {

				CharStorageEntry storedChar = new CharStorageEntry();

				storedChar.width = glyph.width;
				storedChar.height = glyph.height;

				if (positionX + storedChar.width >= textureWidth) {
					positionX = 0;
					positionY += rowHeight;
					rowHeight = 0;
				}

				storedChar.texPosX = positionX;
				storedChar.texPosY = positionY;

				if (storedChar.height > fontHeight) {
					fontHeight = storedChar.height;
				}

				if (storedChar.height > rowHeight) {
					rowHeight = storedChar.height;
				}

				// Draw it here
				g.drawImage(glyph.image, positionX, positionY, null);

				positionX += storedChar.width;

				chars.put(glyph.c, storedChar);
			}

			fontTextureID = loadImage(imgTemp);

			imgTemp = null;

		} catch (Exception e) {
			System.err.println("Failed to create font.");
			e.printStackTrace();
		}
	}

	private void drawQuad(double drawX, double drawY, double drawX2, double drawY2, CharStorageEntry charObj) {
		double srcX = charObj.texPosX + charObj.width;
		double srcY = charObj.texPosY + charObj.height;
		double srcX2 = charObj.texPosX;
		double srcY2 = charObj.texPosY;
		double DrawWidth = drawX2 - drawX;
		double DrawHeight = drawY2 - drawY;
		double TextureSrcX = srcX / textureWidth;
		double TextureSrcY = srcY / textureHeight;
		double SrcWidth = srcX2 - srcX;
		double SrcHeight = srcY2 - srcY;
		double RenderWidth = (SrcWidth / textureWidth);
		double RenderHeight = (SrcHeight / textureHeight);

		drawY -= DrawHeight * clipVerticalB;

		GL11.glTexCoord2d(TextureSrcX, TextureSrcY);
		GL11.glVertex2d(drawX, drawY);
		GL11.glTexCoord2d(TextureSrcX, TextureSrcY + RenderHeight);
		GL11.glVertex2d(drawX, drawY + DrawHeight);
		GL11.glTexCoord2d(TextureSrcX + RenderWidth, TextureSrcY + RenderHeight);
		GL11.glVertex2d(drawX + DrawWidth, drawY + DrawHeight);
		GL11.glTexCoord2d(TextureSrcX + RenderWidth, TextureSrcY);
		GL11.glVertex2d(drawX + DrawWidth, drawY);
	}

	public int getWidth(String whatchars) {
		if (whatchars == null) whatchars = "";
		int totalwidth = 0;
		CharStorageEntry charStorage = null;
		char currentChar = 0;
		for (int i = 0; i < whatchars.length(); i++) {
			currentChar = whatchars.charAt(i);

			charStorage = chars.get(currentChar);

			if (charStorage != null) {
				totalwidth += charStorage.width - correctL;
			}
		}
		return (int) (totalwidth * defScaleX);
	}

	public int getHeight() {
		return (int) (fontHeight * defScaleY * (1 - clipVerticalT - clipVerticalB));
	}

	public int getLineHeight() {
		return getHeight();
	}

	public void drawString(double x, double y, String text, double scaleX, double scaleY, RGB color) {
		drawString(x, y, text, 0, text.length() - 1, scaleX, scaleY, color, LEFT);
	}

	public void drawString(double x, double y, String text, double scaleX, double scaleY, RGB color, int align) {
		drawString(x, y, text, 0, text.length() - 1, scaleX, scaleY, color, align);
	}

	private void drawString(double x, double y, String text, int startIndex, int endIndex, double scaleX, double scaleY, RGB color, int align) {

		scaleX *= defScaleX;
		scaleY *= defScaleY;

		CharStorageEntry charStorage = null;
		int charCurrent;

		int totalwidth = 0;
		int i = startIndex, d = 1, c = correctL;
		float startY = 0;

		switch (align) {
			case RIGHT: {
				d = -1;
				c = correctR;

				while (i < endIndex) {
					if (text.charAt(i) == '\n') startY -= getHeight();
					i++;
				}
				break;
			}
			case CENTER: {
				for (int l = startIndex; l <= endIndex; l++) {
					charCurrent = text.charAt(l);
					if (charCurrent == '\n') break;

					charStorage = chars.get((char) charCurrent);
					if (charStorage != null) {
						totalwidth += charStorage.width - correctL;
					}
				}
				totalwidth /= -2;
				break;
			}
			case LEFT:
			default: {
				d = 1;
				c = correctL;
				break;
			}

		}

		GL11.glBindTexture(GL11.GL_TEXTURE_2D, fontTextureID);
		GL11.glColor4d(color.r, color.g, color.b, color.a);
		GL11.glBegin(GL11.GL_QUADS);

		while (i >= startIndex && i <= endIndex) {

			charCurrent = text.charAt(i);

			charStorage = chars.get(new Character((char) charCurrent));

			if (charStorage != null) {
				if (d < 0) totalwidth += (charStorage.width - c) * d;
				if (charCurrent == '\n') {
					startY -= getHeight() * d;
					totalwidth = 0;
					if (align == CENTER) {
						for (int l = i + 1; l <= endIndex; l++) {
							charCurrent = text.charAt(l);
							if (charCurrent == '\n') break;

							charStorage = chars.get((char) charCurrent);

							totalwidth += charStorage.width - correctL;
						}
						totalwidth /= -2;
					}
					//if center get next lines total width/2;
				} else {

					//@formatter:off
					drawQuad(
								(totalwidth + charStorage.width) * scaleX + x,
								startY * scaleY + y, totalwidth * scaleX + x,
								(startY + charStorage.height)	* scaleY + y,
								charStorage
							);
					//@formatter:on

					if (d > 0) totalwidth += (charStorage.width - c) * d;
				}

			}

			i += d;
		}
		GL11.glEnd();
	}

	public static int loadImage(BufferedImage bufferedImage) {
		try {
			short width = (short) bufferedImage.getWidth();
			short height = (short) bufferedImage.getHeight();
			//textureLoader.bpp = bufferedImage.getColorModel().hasAlpha() ? (byte)32 : (byte)24;
			int bpp = (byte) bufferedImage.getColorModel().getPixelSize();
			ByteBuffer byteBuffer;
			DataBuffer db = bufferedImage.getData().getDataBuffer();
			if (db instanceof DataBufferInt) {
				int intI[] = ((DataBufferInt) (bufferedImage.getData().getDataBuffer())).getData();
				byte newI[] = new byte[intI.length * 4];
				for (int i = 0; i < intI.length; i++) {
					byte b[] = intToByteArray(intI[i]);
					int newIndex = i * 4;

					newI[newIndex] = b[1];
					newI[newIndex + 1] = b[2];
					newI[newIndex + 2] = b[3];
					newI[newIndex + 3] = b[0];
				}

				byteBuffer = ByteBuffer.allocateDirect(width * height * (bpp / 8)).order(ByteOrder.nativeOrder()).put(newI);
			} else {
				byteBuffer = ByteBuffer.allocateDirect(width * height * (bpp / 8)).order(ByteOrder.nativeOrder())
						.put(((DataBufferByte) (bufferedImage.getData().getDataBuffer())).getData());
			}
			byteBuffer.flip();


			int internalFormat = GL11.GL_RGBA8, format = GL11.GL_RGBA;
			IntBuffer textureId = BufferUtils.createIntBuffer(1);

			GL11.glGenTextures(textureId);
			GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId.get(0));


			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP);
			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP);

			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);

			GL11.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);

			GLU.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D, internalFormat, width, height, format, GL11.GL_UNSIGNED_BYTE, byteBuffer);
			return textureId.get(0);

		} catch (Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		return -1;
	}

	public static boolean isSupported(String fontname) {
		Font font[] = getFonts();
		for (int i = font.length - 1; i >= 0; i--) {
			if (font[i].getName().equalsIgnoreCase(fontname)) return true;
		}
		return false;
	}

	public static Font[] getFonts() {
		return GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
	}

	public static Font getFont(String fontname, int style, float size) {
		Font result = null;
		GraphicsEnvironment graphicsenvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
		for (Font font : graphicsenvironment.getAllFonts()) {
			if (font.getName().equalsIgnoreCase(fontname)) {
				result = font.deriveFont(style, size);
				break;
			}
		}
		return result;
	}

	public static byte[] intToByteArray(int value) {
		return new byte[] { (byte) (value >>> 24), (byte) (value >>> 16), (byte) (value >>> 8), (byte) value };
	}

	public void destroy() {
		IntBuffer scratch = BufferUtils.createIntBuffer(1);
		scratch.put(0, fontTextureID);
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
		GL11.glDeleteTextures(scratch);
	}

	public LoadedFont setScale(double x, double y) {
		defScaleX = x;
		defScaleY = y;
		return this;
	}

	public LoadedFont setClip(double clipRatioTop, double clipRatioBottom) {
		clipVerticalT = clipRatioTop;
		clipVerticalB = clipRatioBottom;
		return this;
	}

	public void setCorrection(int correctionLeft, int correctionRight) {
		correctL = correctionLeft;
		correctR = correctionRight;
	}
}
